/*
 * JSFConsole - Lightweight JSF Application Monitor
 * Copyright (C) 2009  Grzegorz Bugaj
 * http://www.gregbugaj.com
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
*/
package com.gregbugaj.jsfconsole.agent;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.WeakHashMap;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;
import javassist.LoaderClassPath;
import javassist.NotFoundException;

import com.gregbugaj.jsfconsole.util.JarUtil;
/**
 * 
 * @author devil
 *
 */
public class PhaseClassTransformer implements ClassFileTransformer {
	public static ClassPool classPool = new ClassPool(true);
	private static WeakHashMap<ClassLoader, Object> classLoaders = new WeakHashMap<ClassLoader, Object> ();


	/**
	 * Name of the map that contains information on how long did it tool for the class to run
	 */


	public byte[] transform(ClassLoader loader, String className,
			Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
			byte[] classfileBuffer) throws IllegalClassFormatException {

		// Check to see if we already know this class loader or add it to the Javassist ClassPool search path if
		// it's new. This is important in J2EE- and Servlet containers that have class loader hierarchies
		if (!classLoaders.containsKey(loader)) {        // <<< TODO: this is a weak hash map, will this really work as intended?
			classPool.insertClassPath(new LoaderClassPath(loader));
			classLoaders.put(loader, null);
		}

		if(!isPhaseEventClass(className)){
			return classfileBuffer;
		}		

		// Parses the byte array into a CtClass instance.
		CtClass ctc=null;
		ctc = loadClass(classfileBuffer, 0, classfileBuffer.length);
		if(ctc==null || ctc.isFrozen()){
			return classfileBuffer;
		}

		System.out.println("*** Candidate = "+ctc.getName());

		byte[] result=null;
		try {
			CtMethod[] methods=ctc.getMethods();
			//			for(CtMethod method:methods){
			//				System.out.println(method.getName());
			//				///addTimerWrapper(ctc, method);
			//			}
			//TODO:Implement JSF Version detection and Instrumentation
			String[] methodNames={"doPhase", "execute"};
			for(String methodName:methodNames){
				try{
					addTimerWrapper(ctc, ctc.getDeclaredMethod(methodName));
					break;
				}catch(Exception ex){
					//ex.printStackTrace();
				}				
			}
			ctc.writeFile();
			result=ctc.toBytecode();
		} catch (NotFoundException e) {
			e.printStackTrace();
		} catch (CannotCompileException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}

		return result;
	}


	private static CtClass loadClass(byte[] classfileBuffer, int offset, int length){
		CtClass cc=null;
		try{
			cc = classPool.makeClass(new ByteArrayInputStream(classfileBuffer, offset, length));
			if(cc.isFrozen()) {
				cc.defrost();
			}
			System.out.println(":::"+cc.getName());
			CtClass ccSuper=cc.getSuperclass();
			if(ccSuper!=null){
				if(ccSuper.getName().equals("java.lang.Object")){
					cc=null;
				}else 
					if(ccSuper.getName().equals("com.sun.faces.lifecycle.Phase") == false){									
						cc=null;
					}			
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return cc;
	}


	private  void addTimerWrapper(CtClass clas, CtMethod originalMethod) throws NotFoundException, CannotCompileException {
		if(originalMethod==null){
			System.out.println("Method is null for class"+clas);
			return;
		}
		System.out.println(" **************** INSTRUMENTING METHOD  ="+originalMethod.getLongName());

		//  get the method information (throws exception if method with
		//  given name is not declared directly by this class, returns
		//  arbitrary choice if more than one with the given name)
		//CtMethod originalMethod = clas.getDeclaredMethod(methodName);
		String methodName=originalMethod.getName();
		//  rename old method to synthetic name, then duplicate the method with original name for use as interceptor
		String newMethodName = methodName+"$impl";
		originalMethod.setName(newMethodName);

		CtMethod newMethod = CtNewMethod.copy(originalMethod, methodName, clas, null);

		//  start the body text generation by saving the start time
		//  to a local variable, then call the timed method; the
		//  actual code generated needs to depend on whether the
		//  timed method returns a value
		String type = originalMethod.getReturnType().getName();
		CtClass classType=originalMethod.getReturnType();

		StringBuffer body = new StringBuffer();
		body.append("{");

		body.append("\nlong start = System.currentTimeMillis();\n");
		body.append("\n javax.faces.context.FacesContext face = (javax.faces.context.FacesContext)$1;\n");
		body.append("\n Integer faceId = new Integer(face.hashCode()); \n");

		body.append("\n  com.gregbugaj.jsfconsole.agent.TraceEventManager.fireEvent(new  com.gregbugaj.jsfconsole.agent.TraceEvent( com.gregbugaj.jsfconsole.agent.EventType.PHASE_STARTED, faceId, new Object[] {getId(), new Boolean(false)}));\n");

		if(classType!=CtClass.voidType){
			body.append(type + " result = ");
		}
		body.append(newMethodName + "($$);\n");

		//Return value if not void
		if(classType!=CtClass.voidType){
			body.append("return result;\n");
		}		

		body.append("\n  com.gregbugaj.jsfconsole.agent.TraceEventManager.fireEvent(new  com.gregbugaj.jsfconsole.agent.TraceEvent( com.gregbugaj.jsfconsole.agent.EventType.PHASE_ENDED, faceId, new Object[] {getId(),  new Boolean(false)}));\n");

		//After the phase have been executed we need to see if the FacesContext.getResponseComplete() is ture
		//and fire dumy event between the given phase and PhaseId.RENDER_RESPONSE so we always 
		//fire complete lifecycle

		InputStream stream=JarUtil.getStreamFromJar("/META-INF/fireevent.jas");
		BufferedReader reader=new BufferedReader(new InputStreamReader(stream));
		String line=null;
		try {
			while((line=reader.readLine())!=null){
				body.append(line);
				body.append("\n");				
			}
		} catch (IOException e) {
			e.printStackTrace();
		}finally{
			try{
				if(reader!=null){
					reader.close();
				}
			}
			catch(Exception ex){}			
		}

		body.append("}");

		//System.out.println(body);
		//  replace the body of the interceptor method with generated  code block and add it to class
		newMethod.setBody(body.toString());
		
		clas.addMethod(newMethod);
	}


	private boolean isPhaseEventClass(String className) {
		/* This  are the Phases
		 *** Candidate = com.sun.faces.lifecycle.RenderResponsePhase
		 *** Candidate = com.sun.faces.lifecycle.RestoreViewPhase
		 *** Candidate = com.sun.faces.lifecycle.ApplyRequestValuesPhase
		 *** Candidate = com.sun.faces.lifecycle.ProcessValidationsPhase
		 *** Candidate = com.sun.faces.lifecycle.UpdateModelValuesPhase
		 *** Candidate = com.sun.faces.lifecycle.InvokeApplicationPhase

		 */
		if(className==null){
			return false;
		}

		//boolean isPhase=className.startsWith("com/sun/faces/lifecycle/Phase");
		boolean isPhase=className.startsWith("com/sun/faces/lifecycle/") && className.endsWith("Phase");

		//className.startsWith("com/sun/faces/lifecycle/Phase");
		return isPhase;
	}

}
